{*******************************************************************************
* The contents of this file are used with permission, subject to the Mozilla   *
* Public License Version 1.1 (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at      *
* http://www.mozilla.org/MPL/                                                  *
*                                                                              *
* Software distributed under the License is distributed on an "AS IS" basis,   *
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for *
* the specific language governing rights and limitations under the License.    *
*                                                                              *
* The Original Code is distributed as part of the "mediate" product and is     *
* Copyright (C) @home media limited. All Rights Reserved.                      *
*                                                                              *
* Email: support@athomemedia.co.uk                                             *
* Web:   http://www.athomemedia.co.uk                                          *
*******************************************************************************}

unit MediaManager;

interface

uses Classes, AHMTypes, Canvas, Playlist, Colour, MediaLocations, Equaliser,
     RadioStations, AVRenderer, Video, Picture, Visualisation, DVD, Radio,
     Empty;

const
  MAXIMUM_VOLUME = 0; // 0dB = No attenuation
  MINIMUM_VOLUME = -50; // -40dB = Silence (apart from in my car, so use -50 :-)
  VOLUME_INCREMENT = 1; // 1dB smallest volume change
  VIDEO_LAUNCH_DURATION = 5; // Time in seconds we allow for video to launch
  MAX_SEEK_ATTEMPTS = 5; // Maximum attempts to seek media position

type
  TAHMAudioMode = (amNone, amVideoStream, amMusic, amRadio);
  TAHMVideoMode = (vmNone, vmVideoStream, vmSlideShow, vmAlbumArt,
                   vmVisualisation, vmDVD);
  TAHMStreamEvent = (sePlay, sePause, seResume, seNew, seStop);

  TAHMMediaStream = class
  private
    FPlaylist: TAHMPlaylist;
    FMediaState: TAHMMediaState;
    FAudioMode: TAHMAudioMode;
    FVideoMode: TAHMVideoMode;
    FVideo: TAHMVideoRenderer;
    FPicture: TAHMPictureRenderer;
    FVisualisation: TAHMVisualisationRenderer;
    FDVD: TAHMDVDRenderer;
    FRadio: TAHMRadioRenderer;
    FEmpty: TAHMEmptyRenderer;
    FBackgroundColour: TAHMCanvasColour;
    FLastError: TAHMMediaError;
  protected
    function GetCurrentRenderer: TAHMAVRenderer;
    procedure SetNewVolume;
    procedure InitialiseCurrentMedia;
    procedure InitialiseMediaMetaData;
    procedure ThrowError;
  public
    constructor Create;
    destructor Destroy; override;
    property Playlist: TAHMPlaylist read FPlaylist;
    property MediaState: TAHMMediaState read FMediaState write FMediaState;
    property AudioMode: TAHMAudioMode read FAudioMode;
    property VideoMode: TAHMVideoMode read FVideoMode;
    property CurrentRenderer: TAHMAVRenderer read GetCurrentRenderer;
    property BackgroundColour: TAHMCanvasColour read FBackgroundColour write FBackgroundColour;
    property LastError: TAHMMediaError read FLastError write FLastError;
    procedure UpdateMediaState;
    procedure SignalEvent(EventType: TAHMMediaSignal);
    procedure Render(Rect: TAHMRectF; Shade: TAHMCanvasShade; Gamma: TAHMCanvasColour;
                     XScale, YScale: Single; XFlip, YFlip: Boolean);
    function Play: Boolean;
    function Pause: Boolean;
    function PlayPause: Boolean;
    function Stop: Boolean;
    function SkipForward: Boolean;
    function SkipBackward: Boolean;
    function FastForward: Boolean;
    function Rewind: Boolean;
    function Mute: Boolean;
    function UnMute: Boolean;
    function MenuActive: Boolean;
    function MenuReturn: Boolean;
    function MenuExit: Boolean;
    function MenuSelect: Boolean; overload;
    function MenuSelect(X, Y: Single): Boolean; overload;
    function MenuNavigate(Direction: TAHMDirection): Boolean; overload;
    function MenuNavigate(X, Y: Single): Boolean; overload;
    procedure ClearPlaylist;
    procedure AddToPlaylist;
    procedure InsertPlaylist;
    procedure AppendPlaylist;
    procedure ReplacePlaylist;
    procedure RestartPlaylist;
    procedure VisualisationQualityChanged;
    procedure EqualiserSettingsChanged;
  end;

  TAHMMediaStreamList = class
  private
    FStreams: TList;
  protected
    function CreateStream: TAHMMediaStream;
    function GetStream(Index: Integer): TAHMMediaStream;
    function GetCount: Integer;
  public
    constructor Create;
    destructor Destroy; override;
    property Count: Integer read GetCount;
  end;

  TAHMMediaManager = class
  private
    FLastMediaPosition: LongInt;
    FLastSeekRequested: LongInt;
    FMediaSeekAttempts: Integer;
    FMediaSelection: TAHMPlaylist;
    FStreamList: TAHMMediaStreamList;
    FMuted: Boolean;
    FVolume: Integer;
    FEqualiser: TAHMEqualiserManager;
    FRadio: TAHMRadioStationManager;
    FMediaLocations: TAHMMediaLocationParser;
  protected
    function GetMediaItem: TAHMMediaItem;
    procedure SetMediaItem(Item: TAHMMediaItem);
    function GetPrimaryStream: TAHMMediaStream;
    function GetSecondaryStream: TAHMMediaStream;
    function GetActiveStream: TAHMMediaStream;
    function GetPrimaryMediaDuration: LongInt;
    function GetSecondaryMediaDuration: LongInt;
    function GetCurrentMediaDuration: LongInt;
    function GetCurrentMediaPosition: LongInt;
    procedure SetCurrentMediaPosition(Position: LongInt);
    procedure SignalStreamEvent(Event: TAHMStreamEvent; Stream: TAHMMediaStream);
    procedure UpdateMediaPosition;
    procedure UpdateMediaVolume;
  public
    constructor Create;
    destructor Destroy; override;
    property Equaliser: TAHMEqualiserManager read FEqualiser;
    property Radio: TAHMRadioStationManager read FRadio;
    property MediaSelection: TAHMPlaylist read FMediaSelection;
    property MediaItem: TAHMMediaItem read GetMediaItem write SetMediaItem;
    property MediaLocations: TAHMMediaLocationParser read FMediaLocations;
    property PrimaryStream: TAHMMediaStream read GetPrimaryStream;
    property SecondaryStream: TAHMMediaStream read GetSecondaryStream;
    property ActiveStream: TAHMMediaStream read GetActiveStream;
    property Muted: Boolean read FMuted;
    property Volume: Integer read FVolume;
    property PrimaryMediaDuration: LongInt read GetPrimaryMediaDuration;
    property SecondaryMediaDuration: LongInt read GetSecondaryMediaDuration;
    property CurrentMediaDuration: LongInt read GetCurrentMediaDuration;
    property CurrentMediaPosition: LongInt read GetCurrentMediaPosition write SetCurrentMediaPosition;
    procedure UpdateMediaState;
    procedure LoadMediaLocations;
    function HasVideoJustStarted: Boolean;
    function Play(Stream: TAHMStreamType): Boolean;
    function Pause(Stream: TAHMStreamType): Boolean;
    function PlayPause(Stream: TAHMStreamType): Boolean;
    function Stop(Stream: TAHMStreamType): Boolean;
    function SkipForward(Stream: TAHMStreamType): Boolean;
    function SkipBackward(Stream: TAHMStreamType): Boolean;
    function FastForward(Stream: TAHMStreamType): Boolean;
    function Rewind(Stream: TAHMStreamType): Boolean;
    function Mute: Boolean;
    function UnMute: Boolean;
    function IncreaseVolume: Boolean;
    function DecreaseVolume: Boolean;
    function SetVolume(Volume: Integer): Boolean;
    function ToggleMute: Boolean;
    function GetPlaylist(Stream: TAHMStreamType): TAHMPlaylist;
    procedure ClearPlaylist(Stream: TAHMStreamType);
    procedure AddToPlaylist(Stream: TAHMStreamType);
    procedure InsertPlaylist(Stream: TAHMStreamType);
    procedure AppendPlaylist(Stream: TAHMStreamType);
    procedure ReplacePlaylist(Stream: TAHMStreamType);
    procedure RestartPlaylist(Stream: TAHMStreamType);
    procedure VisualisationQualityChanged;
    procedure EqualiserSettingsChanged;
  end;


implementation

uses SysUtils, ApplicationManager, Actions, MetaConsts, ErrorCodes,
     Utilities, TagNames, Timing;

//----------------------------------------------------------------------------//

constructor TAHMMediaStream.Create;
begin
  inherited Create;

  // Set initial Media state and modes
  FMediaState := msStopped;
  FAudioMode := amNone;
  FVideoMode := vmNone;

  // Create media stream rendering objects and set stream owner
  FVideo := TAHMVideoRenderer.Create;
  FVideo.SetStreamOwner(Self);
  FPicture := TAHMPictureRenderer.Create;
  FPicture.SetStreamOwner(Self);
  FVisualisation := TAHMVisualisationRenderer.Create;
  FVisualisation.SetStreamOwner(Self);
  FDVD := TAHMDVDRenderer.Create;
  FDVD.SetStreamOwner(Self);
  FRadio := TAHMRadioRenderer.Create;
  FRadio.SetStreamOwner(Self);
  FEmpty := TAHMEmptyRenderer.Create;
  FEmpty.SetStreamOwner(Self);

  // Create playlist and assign to stream renderer
  FPlaylist := TAHMPlaylist.Create;
  FVideo.Playlist := FPlaylist;
  FPicture.Playlist := FPlaylist;
  FVisualisation.Playlist := FPlaylist;
  FDVD.Playlist := FPlaylist;
  FRadio.Playlist := FPlaylist;
  FEmpty.Playlist := FPlaylist;
end;

destructor TAHMMediaStream.Destroy;
begin
  FVideo.Free;
  FPicture.Free;
  FVisualisation.Free;
  FDVD.Free;
  FRadio.Free;
  FEmpty.Free;
  FPlaylist.Free;

  inherited Destroy;
end;

function TAHMMediaStream.GetCurrentRenderer: TAHMAVRenderer;
begin
  case FVideoMode of
    vmVideoStream: Result := FVideo;
    vmSlideShow: Result := FPicture;
    vmVisualisation: if FAudioMode = amRadio then Result := FRadio
                                             else Result := FVisualisation;
    vmDVD: Result := FDVD;
    vmNone: Result := FEmpty;
//    vmAlbumArt: TODO
    else Result := nil;
  end;
end;

procedure TAHMMediaStream.SetNewVolume;
begin
  // Ask current renderer to change its volume if supported
  if GetCurrentRenderer <> nil then
    GetCurrentRenderer.Volume := ApplicationManagerInstance.MediaManager.Volume;
end;

procedure TAHMMediaStream.UpdateMediaState;
begin
  // Ask current renderer to update its internal state
  if GetCurrentRenderer <> nil then
    GetCurrentRenderer.UpdateMediaState;
end;

procedure TAHMMediaStream.SignalEvent(EventType: TAHMMediaSignal);
begin
  // This method is called when one of the renderers wants to signal an event
  case EventType of
    msigNext: if FPlaylist.CanSkipNext then SkipForward else Stop;
    msigPrev: if FPlaylist.CanSkipPrev then SkipBackward else Stop;
    msigEnd: Stop;
    msigAbort: FMediaState := msStopped;
    msigError: Stop;
  end;
end;

procedure TAHMMediaStream.Render(Rect: TAHMRectF; Shade: TAHMCanvasShade;
                                 Gamma: TAHMCanvasColour; XScale, YScale: Single;
                                 XFlip, YFlip: Boolean);
begin
  // Set renderer background colour if required
  if GetCurrentRenderer <> nil then
  begin
    // Set renderer background colour
    GetCurrentRenderer.BackgroundColour := FBackgroundColour;

    // Render stream according to appropriate video mode
    GetCurrentRenderer.Render(Rect, Shade, Gamma, XScale, YScale, XFlip, YFlip);
  end;
end;

procedure TAHMMediaStream.InitialiseCurrentMedia;
var
  Item: TAHMMediaItem;
begin
  // Get current item in playlist (if any)
  Item := FPlaylist.CurrentItem;

  // Determine appropriate media mode for this content
  if not Assigned(Item) then
    FVideoMode := vmNone
  else
    case Item.MediaType of
    mtAudio: FVideoMode := vmVisualisation;
    mtRadio: FVideoMode := vmVisualisation;
    mtImage: FVideoMode := vmSlideShow;
    mtVideo: FVideoMode := vmVideoStream;
    mtDVD:   FVideoMode := vmDVD;
    mtPlaylist: FVideoMode := vmVisualisation;
    else     FVideoMode := vmNone
  end;
  if not Assigned(Item) then
    FAudioMode := amNone
  else
    case Item.MediaType of
    mtAudio: FAudioMode := amMusic;
    mtRadio: FAudioMode := amRadio;
    mtImage: FAudioMode := amNone;
    mtVideo: FAudioMode := amVideoStream;
    mtDVD:   FAudioMode := amVideoStream;
    mtPlaylist: FAudioMode := amMusic;
    else     FAudioMode := amNone;
  end;

  // Initialise volume & equaliser for new media item
  SetNewVolume;

  // Reset last media error property
  FLastError := meNone;
end;

procedure TAHMMediaStream.InitialiseMediaMetaData;
  procedure SetMetaData(MediaPrefix, PlaylistPrefix: String);
  var
    Item: TAHMMediaItem;
  begin
    with ApplicationManagerInstance.MetaDataManager do
    begin
      SetMetaValue(PlaylistPrefix + META_PLAYL_INDEX, FPlaylist.DisplayIndex);
      SetMetaValue(PlaylistPrefix + META_PLAYL_COUNT, FPlaylist.DisplayCount);

      // Do we have a current item in our playlist?
      Item := FPlaylist.CurrentItem;
      if Assigned(Item) then
      begin
        SetMetaWideValue(MediaPrefix + META_MEDIA_URL, Item.URL);
        SetMetaWideValue(MediaPrefix + META_MEDIA_ROOT, Item.Root);
        SetMetaWideValue(MediaPrefix + META_MEDIA_FOLDER, Item.Folder);
        SetMetaWideValue(MediaPrefix + META_MEDIA_FILENAME, Item.Filename);
        SetMetaWideValue(MediaPrefix + META_MEDIA_ICON, Item.Icon);
        SetMetaValue(MediaPrefix + META_MEDIA_NAME, Item.Caption);
        SetMetaValue(MediaPrefix + META_MEDIA_SIZE, FileSizeToString(Item.Size));
        SetMetaValue(MediaPrefix + META_MEDIA_DATE, DateToStr(Item.Timestamp));
        SetMetaValue(MediaPrefix + META_MEDIA_TIME, TimeToStr(Item.Timestamp));
        SetMetaValue(MediaPrefix + META_MEDIA_TYPE, MediaType2Str(Item.MediaType));
        SetMetaValue(MediaPrefix + META_MEDIA_AUTHOR, Item.Author);
        SetMetaValue(MediaPrefix + META_MEDIA_GROUP, Item.Group);
        SetMetaValue(MediaPrefix + META_MEDIA_CATEGORY, Item.Category);
        SetMetaValue(MediaPrefix + META_MEDIA_BITRATE, FrequencyToString(Item.Bitrate));
        SetMetaInteger(MediaPrefix + META_MEDIA_SEQUENCE, Item.Sequence);
        SetMetaInteger(MediaPrefix + META_MEDIA_YEAR, Item.Year);
      end
      else
      begin
        SetMetaValue(MediaPrefix + META_MEDIA_NAME, '');
        SetMetaValue(MediaPrefix + META_MEDIA_URL, '');
        SetMetaValue(MediaPrefix + META_MEDIA_ROOT, '');
        SetMetaValue(MediaPrefix + META_MEDIA_FOLDER, '');
        SetMetaValue(MediaPrefix + META_MEDIA_FILENAME, '');
        SetMetaValue(MediaPrefix + META_MEDIA_ICON, '');
        SetMetaValue(MediaPrefix + META_MEDIA_SIZE, '');
        SetMetaValue(MediaPrefix + META_MEDIA_DATE, '');
        SetMetaValue(MediaPrefix + META_MEDIA_TIME, '');
        SetMetaValue(MediaPrefix + META_MEDIA_TYPE, '');
        SetMetaValue(MediaPrefix + META_MEDIA_AUTHOR, '');
        SetMetaValue(MediaPrefix + META_MEDIA_GROUP, '');
        SetMetaValue(MediaPrefix + META_MEDIA_CATEGORY, '');
        SetMetaValue(MediaPrefix + META_MEDIA_BITRATE, '');
        SetMetaValue(MediaPrefix + META_MEDIA_SEQUENCE, '');
        SetMetaValue(MediaPrefix + META_MEDIA_YEAR, '');
      end;
    end;
  end;
begin
  // Set supporting metadata entries if we're the active stream
  if ApplicationManagerInstance.MediaManager.ActiveStream = Self then
    SetMetaData(META_MEDIA_CURRENT, META_PLAYL_CURRENT);

  // Set supporting metadata entries for primary/secondary stream stuff
  if ApplicationManagerInstance.MediaManager.PrimaryStream = Self then
    SetMetaData(META_MEDIA_PRIMARY, META_PLAYL_PRIMARY)
  else
    SetMetaData(META_MEDIA_SECONDARY, META_PLAYL_SECONDARY);
end;

procedure TAHMMediaStream.ThrowError;
begin
  // Flag media playback error to UI manager to handle
  with ApplicationManagerInstance.UIManager do
    case FLastError of
      meInstallationBroken: DisplayError(ERR_VID_INSTALLATION_TEXT, ERR_VID_INSTALLATION_DESC);
      meUnsupportedMedia: DisplayError(ERR_VID_UNSUPPORTED_TEXT, ERR_VID_UNSUPPORTED_DESC);
      meUnsupportedHardware: DisplayError(ERR_VID_UNSUPPORTEDHW_TEXT, ERR_VID_UNSUPPORTEDHW_DESC);
      meExternalError: DisplayError(ERR_VID_EXTERNAL_TEXT, ERR_VID_EXTERNAL_DESC);
      meStreamEnded: DisplayError(ERR_VID_STREAMENDED_TEXT, ERR_VID_STREAMENDED_DESC);
    end;
end;

function TAHMMediaStream.Play: Boolean;
var
  OldState: TAHMMediaState;
begin
  // Set default result - couldn't play
  Result := False;
  if MediaState = msPlaying then Exit;
  OldState := MediaState;

  // Play first or resume current item in playlist
  if FPlaylist.Empty or FPlaylist.Finished then Exit;
  if not (FPlaylist.HasCurrentItem or FPlaylist.FirstItem) then Exit;

  // Choose appropriate renderer for new media item
  InitialiseCurrentMedia;

  if (GetCurrentRenderer <> nil) and GetCurrentRenderer.Play then
  begin
    Result := True;
    FMediaState := msPlaying;
    InitialiseMediaMetaData;

    // Signal appropriate media events
    if OldState = msPaused then
      ApplicationManagerInstance.MediaManager.SignalStreamEvent(seResume, Self)
    else
      ApplicationManagerInstance.MediaManager.SignalStreamEvent(sePlay, Self);
    ApplicationManagerInstance.MediaManager.SignalStreamEvent(seNew, Self);
  end
  else ThrowError;
end;

function TAHMMediaStream.Pause: Boolean;
begin
  // Set default result - couldn't pause
  Result := False;
  if MediaState <> msPlaying then Exit;

  if (GetCurrentRenderer <> nil) and GetCurrentRenderer.Pause then
  begin
    Result := True;
    FMediaState := msPaused;

    // Signal pause event
    ApplicationManagerInstance.MediaManager.SignalStreamEvent(sePause, Self);
  end;
end;

function TAHMMediaStream.PlayPause: Boolean;
begin
  // Toggle between pause/play states
  if FMediaState = msPaused then Result := Play
                            else Result := Pause;
end;

function TAHMMediaStream.Stop: Boolean;
begin
  // Set default result - couldn't stop
  Result := False;
  if MediaState = msStopped then Exit;

  if (GetCurrentRenderer <> nil) and GetCurrentRenderer.Stop then
  begin
    // Ask renderer to release any held resources
    GetCurrentRenderer.ReleaseResources;

    // End playlist and erase media metadata
    Result := True;
    FMediaState := msStopped;
    FPlaylist.FinishPlaylist;

    // Signal stop to media manager
    ApplicationManagerInstance.MediaManager.SignalStreamEvent(seStop, Self);
  end;
end;

function TAHMMediaStream.SkipForward: Boolean;
var
  OldRenderer: TAHMAVRenderer;
begin
  // Set default result - couldn't skip
  Result := False;
  if MediaState <> msPlaying then Exit;

  // Get next item in playlist if there is one
  if Playlist.NextItem then
  begin
    // Make a note of renderer for current media item
    OldRenderer := GetCurrentRenderer;

    // Choose appropriate renderer for new media item
    InitialiseCurrentMedia;

    // If this content uses a new renderer then stop previous renderer and
    // start the new one, otherwise just tell same renderer to skip forward
    if OldRenderer <> GetCurrentRenderer then
    begin
      if Assigned(OldRenderer) then OldRenderer.Stop;
      if Assigned(OldRenderer) then OldRenderer.ReleaseResources;
      if (GetCurrentRenderer <> nil) and GetCurrentRenderer.Play then Result := True;
    end
    else
      if (GetCurrentRenderer <> nil) and GetCurrentRenderer.SkipForward then Result := True;

    // If we started playing new item populate metadata, otherwise throw UI error
    if Result then
    begin
      InitialiseMediaMetaData;
      ApplicationManagerInstance.MediaManager.SignalStreamEvent(seNew, Self);
    end
    else ThrowError;
  end;
end;

function TAHMMediaStream.SkipBackward: Boolean;
var
  OldRenderer: TAHMAVRenderer;
begin
  // Set default result - couldn't skip
  Result := False;
  if MediaState <> msPlaying then Exit;

  // Get previous item in playlist if there is one
  if Playlist.PreviousItem then
  begin
    // Make a note of renderer for current media item
    OldRenderer := GetCurrentRenderer;

    // Choose appropriate renderer for new media item
    InitialiseCurrentMedia;

    // If this content uses a new renderer then stop previous renderer and
    // start the new one, otherwise just tell same renderer to skip backward
    if OldRenderer <> GetCurrentRenderer then
    begin
      if Assigned(OldRenderer) then OldRenderer.Stop;
      if Assigned(OldRenderer) then OldRenderer.ReleaseResources;
      if (GetCurrentRenderer <> nil) and GetCurrentRenderer.Play then Result := True;
    end
    else
      if (GetCurrentRenderer <> nil) and GetCurrentRenderer.SkipBackward then Result := True;

    // If we started playing new item populate metadata, otherwise throw UI error
    if Result then
    begin
      InitialiseMediaMetaData;
      ApplicationManagerInstance.MediaManager.SignalStreamEvent(seNew, Self);
    end
    else ThrowError;
  end;
end;

function TAHMMediaStream.FastForward: Boolean;
begin
  // Set default result - couldn't fast forward
  Result := False;
  if MediaState <> msPlaying then Exit;

  Result := (GetCurrentRenderer <> nil) and GetCurrentRenderer.FastForward;
end;

function TAHMMediaStream.Rewind: Boolean;
begin
  // Set default result - couldn't rewind
  Result := False;
  if MediaState <> msPlaying then Exit;

  Result := (GetCurrentRenderer <> nil) and GetCurrentRenderer.Rewind;
end;

function TAHMMediaStream.Mute: Boolean;
begin
  Result := (GetCurrentRenderer <> nil) and GetCurrentRenderer.Mute;
end;

function TAHMMediaStream.UnMute: Boolean;
begin
  Result := (GetCurrentRenderer <> nil) and GetCurrentRenderer.UnMute;
end;

function TAHMMediaStream.MenuActive: Boolean;
begin
  Result := (GetCurrentRenderer <> nil) and GetCurrentRenderer.MenuActive;
end;

function TAHMMediaStream.MenuReturn: Boolean;
begin
  Result := (GetCurrentRenderer <> nil) and GetCurrentRenderer.MenuReturn;
end;

function TAHMMediaStream.MenuExit: Boolean;
begin
  Result := (GetCurrentRenderer <> nil) and GetCurrentRenderer.MenuExit;
end;

function TAHMMediaStream.MenuSelect: Boolean;
begin
  Result := (GetCurrentRenderer <> nil) and GetCurrentRenderer.MenuSelect;
end;

function TAHMMediaStream.MenuSelect(X, Y: Single): Boolean;
begin
  Result := (GetCurrentRenderer <> nil) and GetCurrentRenderer.MenuSelect(X, Y);
end;

function TAHMMediaStream.MenuNavigate(Direction: TAHMDirection): Boolean;
begin
  Result := (GetCurrentRenderer <> nil) and GetCurrentRenderer.MenuNavigate(Direction);
end;

function TAHMMediaStream.MenuNavigate(X, Y: Single): Boolean;
begin
  Result := (GetCurrentRenderer <> nil) and GetCurrentRenderer.MenuNavigate(X, Y);
end;

procedure TAHMMediaStream.ClearPlaylist;
begin
  FPlaylist.Clear;
end;

procedure TAHMMediaStream.AddToPlaylist;
var
  sMode: String;
begin
  // Get playlist mode (insert, append or replace) & handle appropriately
  sMode := ApplicationManagerInstance.MetaDataManager.GetMetaValue(META_PLAYL_MODE);
  if sMode = META_PLAYL_INSERT then InsertPlaylist
  else if sMode = META_PLAYL_APPEND then AppendPlaylist
  else ReplacePlaylist;
end;

procedure TAHMMediaStream.InsertPlaylist;
begin
  // If playlist is empty then just treat this as an append
  if FPlaylist.Empty then AppendPlaylist;

  with ApplicationManagerInstance.MediaManager do
  begin
    // Insert the current contents of our media selection playlist
    FPlaylist.InsertList(MediaSelection);
  end;
end;

procedure TAHMMediaStream.AppendPlaylist;
var
  Empty: Boolean;
begin
  // If our playlist is currently empty we'll set the start position below
  Empty := FPlaylist.Empty;

  with ApplicationManagerInstance.MediaManager do
  begin
    // Add the current contents of our media selection playlist
    FPlaylist.AppendList(MediaSelection);

    // If our list was empty and we have a selection then set the play position
    if Empty and Assigned(MediaItem) then
    begin
      // Match items by URL since we may have filtered some media items out
      FPlaylist.CurrentItem := FPlaylist.FindItemByURL(MediaItem.URL);
    end;
  end;
end;

procedure TAHMMediaStream.ReplacePlaylist;
begin
  FPlaylist.Clear;
  AppendPlaylist;
end;

procedure TAHMMediaStream.RestartPlaylist;
var
  Started: Boolean;
  OldState: TAHMMediaState;
  OldRenderer: TAHMAVRenderer;
begin
  // Store current state before we restart
  OldState := MediaState;

  if Playlist.HasCurrentItem then
  begin
    // Make a note of renderer for current media item
    OldRenderer := GetCurrentRenderer;
    Started := False;

    // Choose appropriate renderer for new media item
    InitialiseCurrentMedia;

    // If this content uses a new renderer then stop previous renderer and
    // start the new one, otherwise just tell same renderer to skip forward
    // which will force it to play the new current playlist item
    if OldRenderer <> GetCurrentRenderer then
    begin
      if Assigned(OldRenderer) then OldRenderer.Stop;
      if Assigned(OldRenderer) then OldRenderer.ReleaseResources;
      if (GetCurrentRenderer <> nil) and GetCurrentRenderer.Play then Started := True;
    end
    else
      if (GetCurrentRenderer <> nil) and GetCurrentRenderer.SkipForward then Started := True;

    // If we started playing new item populate metadata, otherwise throw UI error
    if Started then
    begin
      FMediaState := msPlaying;
      InitialiseMediaMetaData;

      // Trigger events to reflect new state transition
      if OldState = msPaused then
        ApplicationManagerInstance.MediaManager.SignalStreamEvent(seResume, Self)
      else if OldState <> msPlaying then
        ApplicationManagerInstance.MediaManager.SignalStreamEvent(sePlay, Self);
      ApplicationManagerInstance.MediaManager.SignalStreamEvent(seNew, Self);
    end
    else
    begin
      // Problem starting new media, leave in a clean state
      FMediaState := msStopped;
      if OldState <> msStopped then
        ApplicationManagerInstance.MediaManager.SignalStreamEvent(seStop, Self);
      ThrowError;
    end;
  end;
end;

procedure TAHMMediaStream.VisualisationQualityChanged;
begin
  // Give visualisation renderer a kick so it reloads & creates new texture
  if GetCurrentRenderer = FVisualisation then
    FVisualisation.ReleaseResources;
end;

procedure TAHMMediaStream.EqualiserSettingsChanged;
begin
  // If we're rendering an audio stream then update dsp settings
  if (FAudioMode <> amNone) then
    GetCurrentRenderer.UpdateRendererSettings;
end;

//----------------------------------------------------------------------------//

constructor TAHMMediaStreamList.Create;
begin
  inherited Create;

  FStreams := TList.Create;
end;

destructor TAHMMediaStreamList.Destroy;
var
  i: Integer;
begin
  for i := 0 to Pred(FStreams.Count) do
    TAHMMediaStream(FStreams.Items[i]).Free;
  FStreams.Free;

  inherited Destroy;
end;

function TAHMMediaStreamList.GetCount: Integer;
begin
  Result := FStreams.Count;
end;

function TAHMMediaStreamList.CreateStream: TAHMMediaStream;
begin
  Result := TAHMMediaStream.Create;
  FStreams.Add(Result);
end;

function TAHMMediaStreamList.GetStream(Index: Integer): TAHMMediaStream;
begin
  if (Index >= 0) and (Index < FStreams.Count) then
    Result := TAHMMediaStream(FStreams.Items[Index])
  else
    Result := nil;
end;

//----------------------------------------------------------------------------//

constructor TAHMMediaManager.Create;
begin
  inherited Create;

  FStreamList := TAHMMediaStreamList.Create;
  FMediaLocations := TAHMMediaLocationParser.Create;
  FMediaSelection := TAHMPlaylist.Create;
  FEqualiser := TAHMEqualiserManager.Create;
  FRadio := TAHMRadioStationManager.Create;

  // Set initial volume and muted status
  FVolume := ApplicationManagerInstance.StateManager.MediaVolume;
  FMuted := ApplicationManagerInstance.StateManager.MediaMuting;

  // Populate initial volume metadata values
  with ApplicationManagerInstance.MetaDataManager do
  begin
    SetMetaInteger(META_MEDIA_CUR_VOLUME, FVolume);
    SetMetaInteger(META_MEDIA_MIN_VOLUME, MINIMUM_VOLUME);
  end;
end;

destructor TAHMMediaManager.Destroy;
begin
  // Save state just before we are destroyed
  with ApplicationManagerInstance do
  begin
    StateManager.MediaVolume := FVolume;
    StateManager.MediaMuting := FMuted;
  end;

  FStreamList.Free;
  FMediaLocations.Free;
  FMediaSelection.Free;
  FEqualiser.Free;
  FRadio.Free;

  inherited Destroy;
end;

function TAHMMediaManager.GetMediaItem: TAHMMediaItem;
begin
  Result := FMediaSelection.CurrentItem;
end;

procedure TAHMMediaManager.SetMediaItem(Item: TAHMMediaItem);
begin
  FMediaSelection.CurrentItem := Item;

  // Populate metadata for our current media item selection
  with ApplicationManagerInstance.MetaDataManager do
    if Assigned(FMediaSelection.CurrentItem) then
    begin
      SetMetaWideValue(META_MEDIA_SEL + META_MEDIA_URL, Item.URL);
      SetMetaWideValue(META_MEDIA_SEL + META_MEDIA_ROOT, Item.Root);
      SetMetaWideValue(META_MEDIA_SEL + META_MEDIA_FOLDER, Item.Folder);
      SetMetaWideValue(META_MEDIA_SEL + META_MEDIA_FILENAME, Item.Filename);
      SetMetaWideValue(META_MEDIA_SEL + META_MEDIA_ICON, Item.Icon);
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_NAME, Item.Caption);
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_SIZE, FileSizeToString(Item.Size));
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_DATE, DateToStr(Item.Timestamp));
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_TIME, TimeToStr(Item.Timestamp));
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_TYPE, MediaType2Str(Item.MediaType));
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_AUTHOR, Item.Author);
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_GROUP, Item.Group);
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_CATEGORY, Item.Category);
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_BITRATE, FrequencyToString(Item.Bitrate));
      SetMetaInteger(META_MEDIA_SEL + META_MEDIA_SEQUENCE, Item.Sequence);
      SetMetaInteger(META_MEDIA_SEL + META_MEDIA_YEAR, Item.Year);
      SetMetaInteger(META_MEDIA_SEL + META_MEDIA_DUR_SEC, Item.Duration);
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_DURATION, TimerInstance.SecondsToString(Item.Duration));
    end
    else
    begin
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_NAME, '');
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_URL, '');
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_ROOT, '');
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_FOLDER, '');
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_FILENAME, '');
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_ICON, '');
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_SIZE, '');
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_DATE, '');
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_TIME, '');
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_TYPE, '');
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_AUTHOR, '');
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_GROUP, '');
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_CATEGORY, '');
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_BITRATE, '');
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_SEQUENCE, '');
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_YEAR, '');
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_DUR_SEC, '');
      SetMetaValue(META_MEDIA_SEL + META_MEDIA_DURATION, '');
    end;
end;

function TAHMMediaManager.GetPrimaryStream: TAHMMediaStream;
begin
  // For now we just create a single stream on demand
  Result := FStreamList.GetStream(0);
  if Result = nil then
  begin
    Result := FStreamList.CreateStream;
  end;
end;

function TAHMMediaManager.GetSecondaryStream: TAHMMediaStream;
begin
  // For now we just create a single stream on demand
  Result := FStreamList.GetStream(1);
  if Result = nil then
  begin
    // If primary stream not yet created then create that first
    if FStreamList.Count = 0 then FStreamList.CreateStream;
    Result := FStreamList.CreateStream;
  end;
end;

function TAHMMediaManager.GetActiveStream: TAHMMediaStream;
begin
  // Determine which stream to display in our video window
  if GetPrimaryStream.MediaState in [msPaused, msSeeking, msPlaying] then
    Result := GetPrimaryStream
  else if GetSecondaryStream.MediaState in [msPaused, msSeeking, msPlaying] then
    Result := GetSecondaryStream
  else
    Result := GetPrimaryStream;
end;

function TAHMMediaManager.GetPrimaryMediaDuration: LongInt;
var
  Stream: TAHMMediaStream;
begin
  Stream := GetPrimaryStream;

  // Get duration from primary stream
  if Stream.CurrentRenderer <> nil then
    Result := Stream.CurrentRenderer.Duration
  else
    Result := 0;
end;

function TAHMMediaManager.GetSecondaryMediaDuration: LongInt;
var
  Stream: TAHMMediaStream;
begin
  Stream := GetSecondaryStream;

  // Get duration from secondary stream
  if Stream.CurrentRenderer <> nil then
    Result := Stream.CurrentRenderer.Duration
  else
    Result := 0;
end;

function TAHMMediaManager.GetCurrentMediaDuration: LongInt;
var
  Stream: TAHMMediaStream;
begin
  // Get active stream, but if it doesn't support duration get other stream
  Stream := GetActiveStream;
  if (Stream.CurrentRenderer = nil) or (Stream.CurrentRenderer.Duration = 0) then
    if Stream = GetPrimaryStream then Stream := GetSecondaryStream else Stream := GetPrimaryStream;

  // Get duration from appropriate stream
  if Stream.CurrentRenderer <> nil then
    Result := Stream.CurrentRenderer.Duration
  else
    Result := 0;
end;

function TAHMMediaManager.GetCurrentMediaPosition: LongInt;
var
  Stream: TAHMMediaStream;
begin
  // Get active stream, but if it doesn't support duration get other stream
  Stream := GetActiveStream;
  if (Stream.CurrentRenderer = nil) or (Stream.CurrentRenderer.Duration = 0) then
    if Stream = GetPrimaryStream then Stream := GetSecondaryStream else Stream := GetPrimaryStream;

  // Get position from appropriate stream
  if Stream.CurrentRenderer <> nil then
    Result := Stream.CurrentRenderer.Position
  else
    Result := 0;
end;

procedure TAHMMediaManager.SetCurrentMediaPosition(Position: LongInt);
var
  Stream: TAHMMediaStream;
begin
  // Get active stream, but if it doesn't support duration get other stream
  Stream := GetActiveStream;
  if (Stream.CurrentRenderer = nil) or (Stream.CurrentRenderer.Duration = 0) then
    if Stream = GetPrimaryStream then Stream := GetSecondaryStream else Stream := GetPrimaryStream;

  // Set position for appropriate stream
  if Stream.CurrentRenderer <> nil then
    Stream.CurrentRenderer.Position := Position;
end;

procedure TAHMMediaManager.SignalStreamEvent(Event: TAHMStreamEvent; Stream: TAHMMediaStream);
var
  Primary: Boolean;
begin
  // Is this event for our primary or secondary stream?
  Primary := (Stream = PrimaryStream);

  // Invoke appropriate theme actions in response to various stream events
  with ApplicationManagerInstance.ActionManager do
    case Event of
      sePlay:
        if Primary then ProcessAction(AC_ONPRIPLAY) else ProcessAction(AC_ONSECPLAY);
      sePause:
        if Primary then ProcessAction(AC_ONPRIPAWS) else ProcessAction(AC_ONSECPAWS);
      seResume:
        if Primary then ProcessAction(AC_ONPRIRSUM) else ProcessAction(AC_ONSECRSUM);
      seNew:
        if Primary then ProcessAction(AC_PRIMEDIA) else ProcessAction(AC_SECMEDIA);
      seStop:
      begin
        if Primary then ProcessAction(AC_ONPRISTOP) else ProcessAction(AC_ONSECSTOP);
        ProcessAction(AC_ONSTOP);

        // Update playlist metadata for both streams when either stream stops
        PrimaryStream.InitialiseMediaMetaData;
        SecondaryStream.InitialiseMediaMetaData;
      end;
    end;
end;

procedure TAHMMediaManager.UpdateMediaPosition;
var
  MediaPos, MediaDur, MediaRem: LongInt;
begin
  with ApplicationManagerInstance.MetaDataManager do
  begin
    // Check for any external changes to media position
    MediaPos := GetMetaInteger(META_MEDIA_CUR_POS_SEC);
    if (MediaPos <> FLastMediaPosition) then
    begin
      // External change so update media position
      SetCurrentMediaPosition(MediaPos);

      // If we are still seeking for the same point then increment our count
      // of seek attempts. This is so that we don't continue seeking forever
      // on media streams that may not have sufficient key frames
      if (FLastSeekRequested = MediaPos) then Inc(FMediaSeekAttempts)
                                         else FMediaSeekAttempts := 0;
      FLastSeekRequested := MediaPos; // update seek point

      // If our media position has caught up or we have waited too long for
      // it to catch up then we update our cached position to allow resuming
      if (FMediaSeekAttempts > MAX_SEEK_ATTEMPTS) or
         (GetCurrentMediaPosition = MediaPos) then
        FLastMediaPosition := MediaPos;
    end
    else
    begin
      // Get current media position from stream
      MediaPos := GetCurrentMediaPosition;
      FLastMediaPosition := MediaPos;
    end;

    // Get media duration and calculate time remaining
    MediaDur := CurrentMediaDuration;
    MediaRem := MediaDur - MediaPos;
    if MediaRem < 0 then MediaRem := 0;

    // Update current media meta data
    with TimerInstance do
    begin
      SetMetaInteger(META_MEDIA_CUR_POS_SEC, MediaPos);
      SetMetaInteger(META_MEDIA_CUR_DUR_SEC, MediaDur);
      SetMetaInteger(META_MEDIA_CUR_REM_SEC, MediaRem);
      SetMetaValue(META_MEDIA_CUR_POSITION, SecondsToString(MediaPos));
      SetMetaValue(META_MEDIA_CUR_DURATION, SecondsToString(MediaDur));
      SetMetaValue(META_MEDIA_CUR_REMAINING, SecondsToString(MediaRem));

      // Update primary and secondary media duration meta data
      MediaDur := PrimaryMediaDuration;
      SetMetaInteger(META_MEDIA_PRIMARY + META_MEDIA_DUR_SEC, MediaDur);
      SetMetaValue(META_MEDIA_PRIMARY + META_MEDIA_DURATION, SecondsToString(MediaDur));
      MediaDur := SecondaryMediaDuration;
      SetMetaInteger(META_MEDIA_SECONDARY + META_MEDIA_DUR_SEC, MediaDur);
      SetMetaValue(META_MEDIA_SECONDARY + META_MEDIA_DURATION, SecondsToString(MediaDur));
    end;
  end;
end;

procedure TAHMMediaManager.UpdateMediaVolume;
var
  MediaVol: Integer;
begin
  with ApplicationManagerInstance.MetaDataManager do
  begin
    // Check for any external changes to media volume
    MediaVol := GetMetaInteger(META_MEDIA_CUR_VOLUME);
    if (MediaVol <> FVolume) then
    begin
      // External change so update media position
      SetVolume(MediaVol);
    end;
  end;
end;

procedure TAHMMediaManager.UpdateMediaState;
var
  PriState, SecState: TAHMMediaState;
  Stream: TAHMMediaStream;
  MenuActive: Boolean;
begin
  // Update media position & volume metadata
  UpdateMediaPosition;
  UpdateMediaVolume;

  with ApplicationManagerInstance.MetaDataManager do
  begin
    // Determine whether DVD menu is currently visible or not
    Stream := GetActiveStream;
    MenuActive := Assigned(Stream) and Stream.MenuActive;
    SetMetaBoolean(META_MEDIA_CUR_DVDMENUACTIVE, MenuActive);

    // Update metadata values indicating current media state
    PriState := PrimaryStream.MediaState;
    SecState := SecondaryStream.MediaState;
    SetMetaBoolean(META_MEDIA_CUR_PLAYING, (PriState in [msSeeking, msPlaying]) or (SecState in [msSeeking, msPlaying]));
    SetMetaBoolean(META_MEDIA_CUR_PAUSED, (PriState = msPaused) or (SecState = msPaused));
    SetMetaBoolean(META_MEDIA_CUR_STOPPED, (PriState = msStopped) and (SecState = msStopped));
  end;

  // Ask primary and secondary media streams to update their internal state
  PrimaryStream.UpdateMediaState;
  SecondaryStream.UpdateMediaState;

  // Update radio RDS metadata
  FRadio.UpdateMetaData;
end;

procedure TAHMMediaManager.LoadMediaLocations;
begin
  // Clear any existing media locations before we start
  FMediaLocations.ClearFolders;

  // Parse XML to populate media locations
  FMediaLocations.TrailingSlashes := True;
  FMediaLocations.ParseXML(ApplicationManagerInstance.ConfigPath);
  FMediaLocations.AppendExternalFolders;
end;

function TAHMMediaManager.HasVideoJustStarted: Boolean;
begin
  // Set default result - we haven't just started playing a video
  Result := False;

  with PrimaryStream do
    if (VideoMode = vmVideoStream) and (MediaState = msPlaying) and
       (CurrentRenderer.Position < VIDEO_LAUNCH_DURATION) then
      Result := True;

  with SecondaryStream do
    if (VideoMode = vmVideoStream) and (MediaState = msPlaying) and
       (CurrentRenderer.Position < VIDEO_LAUNCH_DURATION) then
      Result := True;
end;

function TAHMMediaManager.Play(Stream: TAHMStreamType): Boolean;
begin
  // Determine which streams we want to play
  case Stream of
    stPrimary: Result := PrimaryStream.Play;
    stSecondary: Result := SecondaryStream.Play;
    stSecThenPri: Result := SecondaryStream.Play or PrimaryStream.Play;
    else Result := PrimaryStream.Play or SecondaryStream.Play;
  end;
end;

function TAHMMediaManager.Pause(Stream: TAHMStreamType): Boolean;
begin
  // Determine which streams we want to pause
  case Stream of
    stPrimary: Result := PrimaryStream.Pause;
    stSecondary: Result := SecondaryStream.Pause;
    stSecThenPri: Result := SecondaryStream.Pause or PrimaryStream.Pause;
    else Result := PrimaryStream.Pause or SecondaryStream.Pause;
  end;
end;

function TAHMMediaManager.PlayPause(Stream: TAHMStreamType): Boolean;
begin
  // Determine which streams we want to play/pause
  case Stream of
    stPrimary: Result := PrimaryStream.PlayPause;
    stSecondary: Result := SecondaryStream.PlayPause;
    stSecThenPri: Result := SecondaryStream.PlayPause or PrimaryStream.PlayPause;
    else Result := PrimaryStream.PlayPause or SecondaryStream.PlayPause;
  end;
end;

function TAHMMediaManager.Stop(Stream: TAHMStreamType): Boolean;
begin
  // Determine which streams we want to stop
  case Stream of
    stPrimary: Result := PrimaryStream.Stop;
    stSecondary: Result := SecondaryStream.Stop;
    stSecThenPri: Result := SecondaryStream.Stop or PrimaryStream.Stop;
    else Result := PrimaryStream.Stop or SecondaryStream.Stop;
  end;
end;

function TAHMMediaManager.SkipForward(Stream: TAHMStreamType): Boolean;
begin
  // Determine which streams we want to skip forward
  case Stream of
    stPrimary: Result := PrimaryStream.SkipForward;
    stSecondary: Result := SecondaryStream.SkipForward;
    stSecThenPri: Result := SecondaryStream.SkipForward or PrimaryStream.SkipForward;
    else Result := PrimaryStream.SkipForward or SecondaryStream.SkipForward;
  end;
end;

function TAHMMediaManager.SkipBackward(Stream: TAHMStreamType): Boolean;
begin
  // Determine which streams we want to skip backward
  case Stream of
    stPrimary: Result := PrimaryStream.SkipBackward;
    stSecondary: Result := SecondaryStream.SkipBackward;
    stSecThenPri: Result := SecondaryStream.SkipBackward or PrimaryStream.SkipForward;
    else Result := PrimaryStream.SkipBackward or SecondaryStream.SkipBackward;
  end;
end;

function TAHMMediaManager.FastForward(Stream: TAHMStreamType): Boolean;
begin
  // Determine which streams we want to fast forward
  case Stream of
    stPrimary: Result := PrimaryStream.FastForward;
    stSecondary: Result := SecondaryStream.FastForward;
    stSecThenPri: Result := SecondaryStream.FastForward or PrimaryStream.FastForward;
    else Result := PrimaryStream.FastForward or SecondaryStream.FastForward;
  end;
end;

function TAHMMediaManager.Rewind(Stream: TAHMStreamType): Boolean;
begin
  // Determine which streams we want to rewind
  case Stream of
    stPrimary: Result := PrimaryStream.Rewind;
    stSecondary: Result := SecondaryStream.Rewind;
    stSecThenPri: Result := SecondaryStream.Rewind or PrimaryStream.Rewind;
    else Result := PrimaryStream.Rewind or SecondaryStream.Rewind;
  end;
end;

function TAHMMediaManager.Mute: Boolean;
begin
  // Mute both primary and secondary streams
  if FMuted then
    Result := False
  else
  begin
    ApplicationManagerInstance.ActionManager.ProcessAction(AC_MUTED);
    PrimaryStream.Mute;
    SecondaryStream.Mute;
    FMuted := True;
    Result := True;
  end;
end;

function TAHMMediaManager.UnMute: Boolean;
begin
  // Unmute both primary and secondary streams
  if not FMuted then
    Result := False
  else
  begin
    ApplicationManagerInstance.ActionManager.ProcessAction(AC_UNMUTED);
    PrimaryStream.UnMute;
    SecondaryStream.UnMute;
    FMuted := False;
    Result := True;
  end;
end;

function TAHMMediaManager.ToggleMute: Boolean;
begin
  if FMuted then Result := UnMute
            else Result := Mute;
end;

function TAHMMediaManager.IncreaseVolume: Boolean;
begin
  // Assume we can't increase volume
  Result := False;

  // Increase volume if we can
  if FVolume < MAXIMUM_VOLUME then
  begin
    Inc(FVolume, VOLUME_INCREMENT);
    if FVolume > MAXIMUM_VOLUME then FVolume := MAXIMUM_VOLUME;
    Result := SetVolume(FVolume);
  end;
end;

function TAHMMediaManager.DecreaseVolume: Boolean;
begin
  // Assume we can't decrease volume
  Result := False;

  // Decrease volume if we can
  if FVolume > MINIMUM_VOLUME then
  begin
    Dec(FVolume, VOLUME_INCREMENT);
    if FVolume < MINIMUM_VOLUME then FVolume := MINIMUM_VOLUME;
    Result := SetVolume(FVolume);
  end;
end;

function TAHMMediaManager.SetVolume(Volume: Integer): Boolean;
begin
  // Validate requested volume the update stream volume
  if (Volume >= MINIMUM_VOLUME) and (Volume <= MAXIMUM_VOLUME) then
  begin
    // Cancel mute & update volume level
    FMuted := False;
    FVolume := Volume;

    // Set new volume for primary & secondary streams
    PrimaryStream.SetNewVolume;
    SecondaryStream.SetNewVolume;

    // Update current media volume meta data
    with ApplicationManagerInstance.MetaDataManager do
      SetMetaInteger(META_MEDIA_CUR_VOLUME, FVolume);
    Result := True;
  end
  else
    Result := False;
end;

function TAHMMediaManager.GetPlaylist(Stream: TAHMStreamType): TAHMPlaylist;
begin
  // Return secondary stream if specified, otherwise use primary
  if Stream = stSecondary then Result := SecondaryStream.Playlist
  else Result := PrimaryStream.Playlist;
end;

procedure TAHMMediaManager.ClearPlaylist(Stream: TAHMStreamType);
begin
  // Manage secondary stream if specified, otherwise use primary
  if Stream = stSecondary then SecondaryStream.ClearPlaylist
  else PrimaryStream.ClearPlaylist;
end;

procedure TAHMMediaManager.AddToPlaylist(Stream: TAHMStreamType);
begin
  // Manage secondary stream if specified, otherwise use primary
  if Stream = stSecondary then SecondaryStream.AddToPlaylist
  else PrimaryStream.AddToPlaylist;
end;

procedure TAHMMediaManager.InsertPlaylist(Stream: TAHMStreamType);
begin
  // Manage secondary stream if specified, otherwise use primary
  if Stream = stSecondary then SecondaryStream.InsertPlaylist
  else PrimaryStream.InsertPlaylist;
end;

procedure TAHMMediaManager.AppendPlaylist(Stream: TAHMStreamType);
begin
  // Manage secondary stream if specified, otherwise use primary
  if Stream = stSecondary then SecondaryStream.AppendPlaylist
  else PrimaryStream.AppendPlaylist;
end;

procedure TAHMMediaManager.ReplacePlaylist(Stream: TAHMStreamType);
begin
  // Manage secondary stream if specified, otherwise use primary
  if Stream = stSecondary then SecondaryStream.ReplacePlaylist
  else PrimaryStream.ReplacePlaylist;
end;

procedure TAHMMediaManager.RestartPlaylist(Stream: TAHMStreamType);
begin
  // Restart secondary stream if specified, otherwise use primary
  if Stream = stSecondary then SecondaryStream.RestartPlaylist
  else PrimaryStream.RestartPlaylist;
end;

procedure TAHMMediaManager.VisualisationQualityChanged;
begin
  // Inform both streams that they need to recreate visualisation renderer
  PrimaryStream.VisualisationQualityChanged;
  SecondaryStream.VisualisationQualityChanged;
end;

procedure TAHMMediaManager.EqualiserSettingsChanged;
begin
  // Inform both streams that they need to update equaliser settings
  PrimaryStream.EqualiserSettingsChanged;
  SecondaryStream.EqualiserSettingsChanged;
end;


end.
